#region The MIT License
// The MIT License
// 
// Copyright (c) 2009 octalforty studios
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#endregion
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Diagnostics;
using System.IO;

using octalforty.Componento.ComponentModel.Installation.FileSystem;
using octalforty.Componento.ComponentModel.Tracing;
using octalforty.Componento.Shared.SemanticModel;
using octalforty.Componento.Util;

namespace octalforty.Componento.ComponentModel.Installation
{
    /// <summary>
    /// Standard implementation of the <see cref="IComponentInstallerService"/>.
    /// </summary>
    [Export(typeof(IComponentInstallerService))]
    public class ComponentInstallerService : IComponentInstallerService
    {
        #region Public Properties
        public ITrace Trace 
        { get; set; }

        public ITraceManagerService TraceManager
        { get; set; }

        [Import()]
        public IFileSystemManagerService FileSystemManager 
        { get; set; }

        [Import()]
        public IComponentManifestProviderService ComponentManifestProvider 
        { get; set; }

        /// <summary>
        /// Gets or sets a reference to the <see cref="IResourceInstallerService"/>.
        /// </summary>
        [Import()]
        public IResourceInstallerService ResourceInstaller 
        { get; set; }

        /// <summary>
        /// Gets or sets a reference to the <see cref="IResourceProviderService"/>.
        /// </summary>
        [Import()]
        public IResourceProviderService ResourceProvider 
        { get; set; }

        [Import()]
        public IInstalledComponentRegistryService InstalledComponentRegistry 
        { get; set; }

        [Import()]
        public IComponentDependenciesProviderService ComponentDependenciesProvider 
        { get; set; }
        #endregion

        [ImportingConstructor]
        public ComponentInstallerService(ITraceManagerService traceManager)
        {
            TraceManager = traceManager;
            Trace = TraceManager.GetTrace(GetType().Name);
        }
        
        #region IComponentInstallerService Members
        /// <summary>
        /// Occurs before a component is installed.
        /// </summary>
        public event InstallingComponentEventHandler InstallingComponent;

        /// <summary>
        /// Occurs before a component is uninstalled.
        /// </summary>
        public event UninstallingComponentEventHandler UninstallingComponent;

        /// <summary>
        /// Installs package <paramref name="componentName"/> from <paramref name="uri"/>.
        /// </summary>
        /// <param name="componentName"></param>
        /// <param name="uri"></param>
        /// <param name="versionSpecification"></param>
        public void Install(string componentName, Uri uri, VersionSpecification versionSpecification)
        {
            #region Tracing
            Trace.Info("Installing Component '{0}' from URI '{1}'", componentName, uri);
            #endregion

            var installedComponents = new List<string>();

            try
            {
                #region Tracing
                Trace.Info( "Started transacted installation");
                TraceManager.BeginLogicalOperation();
                #endregion

                InstallTransacted(componentName, uri, versionSpecification, installedComponents);

                #region Tracing
                TraceManager.EndLogicalOperation();
                Trace.Info( "Completed transacted installation");
                #endregion
            } // try

            catch(ComponentInstallationException e)
            {
                #region Tracing
                Trace.Warning( "Rolling back transacted installation");
                TraceManager.BeginLogicalOperation();
                #endregion

                //
                // Rolling back installed components
                installedComponents.Reverse();

                foreach(string installedComponent in installedComponents)
                    Uninstall(installedComponent, ComponentUninstallationFlags.Force | ComponentUninstallationFlags.Recursive);

                #region Tracing
                TraceManager.EndLogicalOperation();
                Trace.Warning( "Rolled back transacted installation");
                #endregion

                throw new ComponentInstallationException(string.Format("Could not install '{0}' from '{1}'", componentName, uri), e);
            } // catch
        }

        private void InstallTransacted(string componentName, Uri uri, VersionSpecification versionSpecification, List<string> installedComponents)
        {
            #region Tracing
            Trace.Verbose( "Retrieving Component Manifest for '{0}' from '{1}'", componentName, uri);
            TraceManager.BeginLogicalOperation();
            #endregion

            var manifest = ComponentManifestProvider.GetComponentManifest(componentName, uri, versionSpecification);
            if(manifest == null)
                throw ExceptionUtil.ComponentInstallationException(null, 
                    Properties.Resources.CouldNotRetrieveComponentManifestFrom, componentName, uri);

            #region Tracing
            TraceManager.EndLogicalOperation();
            Trace.Verbose( "Retrieved Component Manifest for '{0}' from '{1}'. {2} Resources, {3} Dependencies in Manifest", componentName, uri,
                manifest.Resources.Count, manifest.Dependencies.Count);
            Trace.Verbose( "Querying local Registry");
            TraceManager.BeginLogicalOperation();
            #endregion

            var component = InstalledComponentRegistry.GetComponentReference(componentName);

            //
            // Check if component with the same name is already installed
            if(component != null)
            {
                #region Tracing
                Trace.Verbose( "Queried local Registry. Retrieved Component Reference '{0}' {1}", component.Name, component.Version);
                #endregion

                if(component.Version != manifest.Descriptor.Version)
                {
                    #region Tracing
                    Trace.Error( "Local Registry contains an entry for '{0}' {1}, which conflicts with '{2}' {3}",
                        component.Name, component.Version, componentName, manifest.Descriptor.Version);
                    #endregion

                    throw new ComponentInstallationException();
                } // if

                //
                // If versions are equal, do nothing
                if(component.Version == manifest.Descriptor.Version)
                    return;
            } // if

            #region Tracing
            TraceManager.EndLogicalOperation();
            Trace.Verbose( "Local Registry does not contain entries for '{0}'", componentName);
            #endregion


            if(!RaiseInstallingComponent(new InstallingComponentEventArgs(manifest)))
            {
                #region Tracing
                Trace.Verbose( "Installation of '{0}' cancelled", componentName);
                #endregion

                return;
            } // if

            #region Tracing
            Trace.Verbose( "Retrieving Resources");
            TraceManager.BeginLogicalOperation();
            #endregion

            var resources = ResourceProvider.GetResources(manifest, versionSpecification);

            #region Tracing
            TraceManager.EndLogicalOperation();
            Trace.Verbose( "Retrieved {0} Resources", resources.Count);
            #endregion

            var cli = CliType.NetFramework;
            var version = Environment.Version;
            var isa = IsaType.Cil;

            #region Tracing
            Trace.Verbose( "Selecting Path Mapping for '{0}' {1} ({2})", cli, version, isa);
            #endregion

            var selector = new PathMappingSelector();
            var pathMapping = 
                selector.SelectPathMapping(manifest.PathMappings, new CliEnvironment(cli, version), isa);

            #region Tracing
            Trace.Verbose( "Started installing Resources");
            TraceManager.BeginLogicalOperation();
            #endregion

            InstallResources(componentName, resources, pathMapping);

            #region Tracing
            TraceManager.EndLogicalOperation();
            Trace.Verbose( "Completed installing Resources");
            Trace.Verbose( "Registering Component '{0}' {1} in local Registry", componentName, manifest.Descriptor.Version);
            TraceManager.BeginLogicalOperation();
            #endregion

            InstalledComponentRegistry.RegisterComponent(new ComponentReference(componentName, manifest.Descriptor.Version, uri));
            installedComponents.Add(componentName);

            //
            // Installing dependencies
            #region Tracing
            TraceManager.EndLogicalOperation();
            Trace.Verbose( "Querying for Dependencies"); 
            TraceManager.BeginLogicalOperation();
            #endregion

            ComponentReference[] dependencies = ComponentDependenciesProvider.GetComponentDependencies(manifest);
            if(dependencies.Length == 0)
            {
                #region Tracing
                Trace.Verbose( "No Dependencies required");
                TraceManager.EndLogicalOperation();
                #endregion
                
                return;
            } // if

            #region Tracing
            TraceManager.EndLogicalOperation();
            Trace.Verbose( "'{0}' {1} requires {2} Dependencies", componentName, manifest.Descriptor.Version, dependencies.Length);
            Trace.Verbose( "Started installing Dependencies");
            TraceManager.BeginLogicalOperation();
            #endregion

            foreach(ComponentReference dependency in dependencies)
            {
                #region Tracing
                Trace.Verbose( "Installing '{0}' {1} as a Dependency of '{2}' {3}", dependency.Name, dependency.Version,
                    componentName, manifest.Descriptor.Version);
                #endregion

                //
                // If dependeny is already installed, check for version conflicts
                ComponentReference installedComponent = InstalledComponentRegistry.GetComponentReference(dependency.Name);
                if (installedComponent != null)
                {
                    if (installedComponent.Version != null && dependency.Version != null &&
                        dependency.Version != new Version(0, 0, 0, 0) && installedComponent.Version != dependency.Version)
                    {
                        #region Tracing
                        Trace.Error( "Version conflict: '{0}' {1} requires '{2}' {3}, but there is already '{4}' {5} installed",
                                componentName, manifest.Descriptor.Version, dependency.Name, dependency.Version, installedComponent.Name, installedComponent.Version);
                        #endregion

                        throw new VersionConflictException(
                            string.Format("Version conflict: {0} requires {1} version {2}, but there is already version {3} installed",
                                componentName, dependency.Name, dependency.Version, installedComponent.Version));
                    } // if
                } // if

                if(installedComponent == null)
                {
                    try
                    {
                        InstallDependency(componentName, dependency, installedComponents);
                    } // try
                    catch(Exception e)
                    {
                        throw ExceptionUtil.ComponentInstallationException(e, 
                            Properties.Resources.CouldNotInstallDependency, dependency.Name, componentName);
                    } // catch
                } // if

                #region Tracing
                TraceManager.EndLogicalOperation();
                Trace.Verbose( "Installed '{0}' {1} as a Dependency of '{2}' {3}", dependency.Name, dependency.Version,
                    componentName, manifest.Descriptor.Version);
                #endregion
            } // foreach
        }

        private void InstallResources(string componentName, IEnumerable<Uri> resources, PathMapping pathMapping)
        {
            var targetPath = Path.Combine(FileSystemManager.CurrentDirectory, componentName);
            if(!FileSystemManager.DirectoryExists(targetPath))
                FileSystemManager.CreateDirectory(targetPath);

            ResourceInstaller.InstallResources(resources, targetPath, pathMapping);
        }

        private void InstallDependency(string packageName, ComponentReference dependency, List<string> installedComponents)
        {
            InstallTransacted(dependency.Name, dependency.Uri,
                dependency.Version == null ?
                    VersionSpecification.GreaterOrEqual(new Version(0, 0, 0, 0)) :
                    VersionSpecification.Equal(dependency.Version), installedComponents);

            InstalledComponentRegistry.RegisterComponentDependencies(packageName, new string[] { dependency.Name });
        }

        /// <summary>
        /// Uninstalls package <paramref name="componentName"/>.
        /// </summary>
        /// <param name="componentName"></param>
        /// <param name="flags"></param>
        public void Uninstall(string componentName, ComponentUninstallationFlags flags)
        {
            ComponentReference cref = InstalledComponentRegistry.GetComponentReference(componentName);
            if(cref == null)
                return;

            if(InstalledComponentRegistry.GetComponentDependants(componentName).Length > 0 && 
                (flags & ComponentUninstallationFlags.Force) != ComponentUninstallationFlags.Force)
                throw new ComponentUninstallationException();

            string[] dependencies = InstalledComponentRegistry.GetComponentDependencies(componentName);
            if((flags & ComponentUninstallationFlags.Recursive) == ComponentUninstallationFlags.Recursive)
                foreach(string dependency in dependencies)
                    Uninstall(dependency, flags | ComponentUninstallationFlags.Force);

            var args = new UninstallingComponentEventArgs(cref);
            InvokeUninstallingComponent(args);

            InstalledComponentRegistry.UnregisterComponent(componentName);

            string destinationPath = Path.Combine(FileSystemManager.CurrentDirectory, componentName);
            
            if(FileSystemManager.DirectoryExists(destinationPath))
                FileSystemManager.DeleteDirectory(destinationPath);
        }
        #endregion

        private bool RaiseInstallingComponent(InstallingComponentEventArgs args)
        {
            InstallingComponentEventHandler component = InstallingComponent;
            if(component != null) 
                component(this, args);

            return !args.Cancel;
        }

        private void InvokeUninstallingComponent(UninstallingComponentEventArgs args)
        {
            UninstallingComponentEventHandler component = UninstallingComponent;
            if(component != null) 
                component(this, args);
        }
    }

    public class ComponentUninstallationException : Exception
    {
    }

    public class ComponentInstallationException : Exception
    {
        public ComponentInstallationException()
        {
        }

        public ComponentInstallationException(string message) : base(message)
        {
        }

        public ComponentInstallationException(string message, Exception innerException) : base(message, innerException)
        {
        }
    }

    public class VersionConflictException : Exception
    {
        public VersionConflictException()
        {
        }

        public VersionConflictException(string message) : base(message)
        {
        }

        public VersionConflictException(string message, Exception innerException) : base(message, innerException)
        {
        }
    }
}
